package com.gitee.wsl.jvm.okhttp.response

import com.gitee.wsl.jvm.moshi.MoshiExt
import com.gitee.wsl.jvm.okhttp.base.ContentRange
import com.gitee.wsl.jvm.okhttp.base.StatusCode
import com.gitee.wsl.jvm.okhttp.exception.InconsistentContentRangeException
import com.gitee.wsl.jvm.okhttp.exception.ResponseStatusException
import com.squareup.moshi.JsonAdapter
import okhttp3.Response
import okhttp3.ResponseBody
import okhttp3.internal.http.StatusLine
import okio.IOException
import okio.Sink
import okio.sink
import java.io.File
import java.io.FileOutputStream
import java.lang.reflect.Type
import java.net.HttpURLConnection


/**
 *
 * @FileName:
 *          cn.netdiscovery.http.core.extension.`Response+Extension`
 * @author: Tony Shen
 * @date: 2020-10-03 11:11
 * @version: V1.0 <描述当前版本功能>
 */
fun Response.stringBody(): String? = this.body?.string()

fun <T> Response.closeAfter(block: (response: Response) -> T): T = block.invoke(this).apply { close() }

fun Response.applyBody(block: (body: ResponseBody?) -> Unit) {
    block.invoke(this.body)
}

fun Response.applyStringBody(block: (body: String?) -> Unit) {
    block.invoke(this.stringBody())
}

fun <T> Response.letBody(block: (body: ResponseBody?) -> T): T = block.invoke(this.body)

fun <T> Response.letStringBody(block: (body: String?) -> T): T = block.invoke(this.stringBody())

fun <T> Response.mapper(mapper: ResponseMapper<T>): T = mapper.map(this)

inline fun <reified T : Any> Response.toMoshi(): Result<T> = toMoshi(T::class.java)

fun <T : Any> Response.toMoshi(clazz: Class<T>): Result<T> =
    toMoshi(MoshiExt.defaultMoshiBuilder.build().adapter(clazz))

fun <T : Any> Response.toMoshi(type: Type): Result<T> =
    toMoshi(MoshiExt.defaultMoshiBuilder.build().adapter(type))

fun <T : Any> Response.toMoshi(jsonAdapter: JsonAdapter<T>): Result<T> =
    runCatching {
        body?.let { responseBody ->
            jsonAdapter.fromJson(responseBody.source())?.let {
                return Result.success(it)
            }
        }
        return Result.failure(java.lang.RuntimeException("to moshi false"))
    }


/** Returns true if the response must have a (possibly 0-length) body. See RFC 7231.  */
fun Response.hasBody(): Boolean {
    // HEAD requests never yield a body regardless of the response headers.
    if (request.method == "HEAD") {
        return false
    }

    val responseCode = code
    if ((responseCode < StatusLine.HTTP_CONTINUE || responseCode >= HttpURLConnection.HTTP_OK) &&
        (responseCode != HttpURLConnection.HTTP_NO_CONTENT) &&
        (responseCode != HttpURLConnection.HTTP_NOT_MODIFIED)
    ) {
        return true
    }

    // If the Content-Length or Transfer-Encoding headers disagree with the response code, the
    // response is malformed. For best compatibility, we honor the headers.
    return ((contentLength > 0) || isChunked)
}

val Response.contentLength: Long
    get() {
        return this.header("Content-Length")?.toLongOrNull() ?: -1L
    }

val Response.isChunked: Boolean
    get() {
        return this.header("Transfer-Encoding").equals("chunked", ignoreCase = true)
    }

val Response.contentType: String?
    get() {
        return this.header("Content-Type")
    }

/**
 * Returns the [ContentRange] of a `206 Partial Content` response transferring a single part.
 * If the response doesn't have a valid `Content-Range` header field, returns `null`.
 *
 * Specification: [RFC 9110, Section 14.4](https://www.rfc-editor.org/rfc/rfc9110.html#section-14.4)
 */
fun Response.mayGetSinglePartContentRange(): ContentRange? {
    // A single-part partial content response must have a Content-Range header field.
    val headerValue = header("Content-Range") ?: return null

    // Parse the Content-Range header value.
    // cf. https://www.rfc-editor.org/rfc/rfc9110.html#section-14.4
    val result = validRangeRegex.matchEntire(headerValue) ?: return null
    val start = result.groupValues[1].toLongOrNull()?.takeIf { it >= 0 } ?: return null
    val end = result.groupValues[2].toLongOrNull()?.takeIf { it >= start } ?: return null
    val totalLength = when (val total = result.groupValues[3]) {
        "*" -> -1
        else -> total.toLongOrNull()?.takeIf { it > end } ?: return null
    }
    return ContentRange(start, end, totalLength)
}

/**
 * Returns the length of the document specified in the `Content-Range` header field of
 * a `416 Range Not Satisfiable` response.
 * If the response doesn't have an unsatisfied `Content-Range` header field, returns `null`.
 *
 * Specification: [RFC 9110, Section 14.4](https://www.rfc-editor.org/rfc/rfc9110.html#section-14.4)
 */
fun Response.mayGetTotalLengthOfUnsatisfiedRange(): Long? {
    // A "416 Range Not Satisfiable" response should have a Content-Range header field
    // with an unsatisfied-range value.
    val headerValue = header("Content-Range") ?: return null

    // Parse the Content-Range header value.
    // cf. https://www.rfc-editor.org/rfc/rfc9110.html#section-14.4
    val result = unsatisfiedRangeRegex.matchEntire(headerValue) ?: return null
    return result.groupValues[1].toLongOrNull()?.takeIf { it >= 0 }
}

private val validRangeRegex: Regex = Regex("""bytes (\d+)-(\d+)/(\d+|\*)""")
private val unsatisfiedRangeRegex: Regex = Regex("""bytes \*/(\d+)""")

/**
 * Saves the response body to the given [WritableResource] with handling a single-part partial
 * response defined in [RFC 9110, Section 14](https://www.rfc-editor.org/rfc/rfc9110.html#section-14).
 *
 * This is a sample code that performs a *resumable download* using Range requests defined in
 * RFC 9110, Section 14.
 * ```
 * suspend fun performResumableDownloadWithProgress(url: HttpUrl, fileToSave: File) {
 *     val resourceToSave = fileToSave.asWritableResource()
 *     val request = Request.Builder()
 *         .url(url)
 *         .setRangeHeader(resourceToSave.length)
 *         .build()
 *     okHttpClient.callWithCounting(request) { response ->
 *         response.saveToResourceWithSupportingResumption(resourceToSave)
 *     }.collect { state ->
 *         when (state) {
 *             is Uploading -> Unit
 *             is Downloading ->
 *                 println("Downloading: ${state.bytesTransferred} bytes downloaded." +
 *                     state.progressPercentage?.let { " ($it%)" }.orEmpty())
 *             is Success ->
 *                 if (state.data)
 *                     println("The download is complete. TotalLength=${state.bytesDownloaded}")
 *                 else
 *                     println("The HTTP call was successful, but the content may have a remaining part.\n" +
 *                         "To complete the download, call this function again.")
 *             is Failure ->
 *                 println("Failure: ${state.exception}\nTo resume the download, call this function again.")
 *         }
 *     }
 * }
 * ```
 *
 * @param resourceToSave the resource to save the downloaded content.
 * @return `true` if the download of the entire content is completed. `false` if the response is
 * `206 Partial Content` and there may be remaining undownloaded content.
 * @throws InconsistentContentRangeException if the response code is `206 Partial Content` or
 * `416 Range Not Satisfiable`, and the `Content-Range` header value does not match the length of
 * [resourceToSave].
 * @throws ResponseStatusException if the response code is neither `200 OK` nor
 * `206 Partial Content` nor `416 Range Not Satisfiable`.
 * @throws IOException if any other I/O error occurs.
 */
fun Response.saveToResourceWithSupportingResumption(resourceToSave: WritableResource): Boolean {
    val sink: Sink
    val isLastPart: Boolean

    when (code) {
        StatusCode.OK -> {
            // The response body contains the entire content.
            sink = resourceToSave.newSink(append = false)
            isLastPart = true
        }
        StatusCode.PARTIAL_CONTENT -> {
            // cf. https://www.rfc-editor.org/rfc/rfc9110.html#section-15.3.7
            val contentRange = mayGetSinglePartContentRange()
                ?: throw InconsistentContentRangeException("No valid Content-Range header in the `Partial Content` response.")
            if (contentRange.start != resourceToSave.length) {
                throw InconsistentContentRangeException("The start offset of the partial response does not match the length of the resource.")
            }
            // The response body contains the continuation of the resource.
            sink = resourceToSave.newSink(append = true)
            isLastPart = contentRange.isLastPart
        }
        StatusCode.RANGE_NOT_SATISFIABLE -> {
            // cf. https://www.rfc-editor.org/rfc/rfc9110.html#section-15.5.17
            val totalLength = mayGetTotalLengthOfUnsatisfiedRange()
                ?: throw InconsistentContentRangeException("No unsatisfied Content-Range header in the `Range Not Satisfiable` response.")
            if (totalLength != resourceToSave.length) {
                throw InconsistentContentRangeException("The content length of the `Range Not Satisfiable` response does not match the length of the resource.")
            }
            // The entire content was already downloaded to the resource.
            return true
        }
        else -> throw ResponseStatusException(code)
    }

    sink.use { checkNotNull(body).source().readAll(it) }

    return isLastPart
}

/**
 * Saves the response body to [fileToSave] **atomically**.
 *
 * [fileToSave] is updated if and only if the entire response body has been successfully downloaded.
 * Otherwise, [fileToSave] will not be modified at all.
 *
 * This is a sample code that downloads a content of the given URL to a file.
 * ```
 * suspend fun performDownloadAtomically(url: HttpUrl, fileToSave: File): Boolean {
 *     val request = Request.Builder().url(url).build()
 *     return try {
 *         okHttpClient.call(request) { response ->
 *             if (response.code != StatusCode.OK) {
 *                 throw ResponseStatusException(response.code)
 *             }
 *             response.saveBodyToFileAtomically(fileToSave)
 *         }
 *         // At this point, `fileToSave` contains the complete response body downloaded.
 *         true
 *     } catch (e: IOException) {
 *         println("Failed to download: $e")
 *         // At this point, `fileToSave` is not modified at all.
 *         false
 *     }
 * }
 * ```
 *
 * Implementation note: This function is inspired by `android.util.AtomicFile`, but does not depend
 * on any Android API. Technically, this atomicity is implemented by writing the response body to
 * a temporary file and then renaming it. The name of this temporary file is given by appending
 * [suffixForTmpFile] to the path of [fileToSave].
 *
 * @param fileToSave the file to save the downloaded content.
 * @param suffixForTmpFile the suffix to be appended to the temporary file.
 * @throws IOException if any I/O error occurs. In this case, [fileToSave] is not modified at all.
 */
fun Response.saveBodyToFileAtomically(fileToSave: File, suffixForTmpFile: String = ".new") {
    if (suffixForTmpFile.isEmpty()) {
        throw IllegalArgumentException("suffixForTmpFile must not be empty.")
    }

    val tmpFile = File(fileToSave.path + suffixForTmpFile)
    tmpFile.parentFile?.mkdirs()
    val tmpFileOutputStream = FileOutputStream(tmpFile)
    try {
        tmpFileOutputStream.use {
            val responseBody = body ?: throw IOException("This response has no body.")
            responseBody.source().readAll(it.sink())
            it.flush()
            it.fd.sync()
        }
        if (!tmpFile.renameTo(fileToSave)) {
            throw IOException("Failed to rename the tmpFile to $fileToSave")
        }
    } catch (e: IOException) {
        tmpFile.delete()
        throw e
    }
}